//
//  Utilities.m
//  This file contains classes that are used can
//      be used for solving gas related problems
//
//  The following are the class names:
//      DiveCalcs -- Added 21/04/11
//      GasMix -- Added 21/04/11
//
//  Revision History: 22/04/11 Dan MacKay
//              Removed class DiveCalcs and added it's class
//              methods to GasMix.
//          23/04/11 Dan MacKay - Added DOxygen formatting
//              for documentation
//          23/04/11 Dan MacKay - Finished the implementation
//              of the initWithStdGasForDepth: constructor
//
//  Created by Donald MacKay on 11-04-21.
//  Copyright 2011 H2OWorx.com. All rights reserved.
//

#import "DiveUtilities.h"

/***
 * These strings are used for the NSError Object. Eventually
 * they should be moved to the constants.m file of the
 * DecoPlanner project
 */
NSString *const kFO2TooLow      = @"Fraction of Oxygen too low.";
NSString *const kFHeTooLow      = @"Fraction of Helium too low.";
NSString *const kFO2TooHi       = @"Fraction of Oxygen too high.";
NSString *const kFHeTooHi       = @"Fraction of Helium too high.";
NSString *const kDepthTooShallow= @"Requested depth too shallow.";
NSString *const kDepthTooDeep   = @"Requested depth beyond the GUE standard gas range.";
NSString *const kImpossibleMix  = @"Requested mix cannot be made. fO2 and fHe add up to more than 1.0";


/**
 *  GasMix Class. This class models a gas blend by tracking the O2 and Helium
 *      content of the mix.
 *
 *  Created: 21/04/11 DBM
 *
 *  Methods: +(long) ENDofGas: (double) fractionOfHelium atDepthOf: (double) depth isMetric: (BOOL) metric
 *           +(long) EADofGas: (double) fractionOfNitrogen atDepthOf: (double) depth isMetric: (BOOL) metric 
 *           -(double) MOD
 *           -(double) ppO2
 *           -(double) fO2
 *
 *  Revision History: 22/04/11 Dan MacKay
 *      Added the following constructors:
 *          - (id) initWithStdGasForDepth:(double) depth isMetric: (BOOL) metric;
 *          - (id) initAsTriOx;
 *          - (id) initAs21_35;
 *          - (id) initAs18_45;
 *          - (id) initAs15_55;
 *          - (id) initAs12_65;
 *          - (id) initAs10_70;
 */

@implementation GasMix


@synthesize oxygenContent, heliumContent, nitrogenContent;

- (id)init {
    self = [super init];
    if (self) {
    }
    return self;
}

/**
 *  A Nitrox Constructor. Create a Nitrox object by supplying
 *      the fraction of Oxygen.
 *
 *  @param withFractionOfO2 a double argument representing the fraction of Oxygen required.
 *  @param error a NSError pointer in the event of initialization failure. The returned
 *              error object will contain a description of the problem,
 *
 *  @return id if successful otherwise nil
 */
- (id)initAsNitrox: (double) withFractionOfO2 error:(NSError **)error  {
    self = [super init];
    if (self) {
        /***
         * Range Chech the O2 content should be between 32% and 100%
         */
        int tmpFO2 = round(withFractionOfO2*100); // Get rid of any floaty fractions
        if (tmpFO2 < NITROX32_PERCENT) {
            *error = [NSError errorWithDomain:kFO2TooLow code:FO2_TOO_LOW userInfo:nil]; 
            return nil;
        }
        else if (tmpFO2 > OXYGEN100) {
            *error = [NSError errorWithDomain:kFO2TooHi code:FO2_TOO_HI userInfo:nil]; 
            return nil;
        }
        /***
         * FO2 is in range so go ahead and build the object
         */
        oxygenContent = [NSNumber numberWithDouble:withFractionOfO2];
        heliumContent = [NSNumber numberWithDouble:0.0];
        nitrogenContent = [NSNumber numberWithDouble:1.0 - [oxygenContent doubleValue]];
    }
    return self;
}

/**
 * A Trimix Constructor. Creat a Trimix object by suppling
 *      the fraction of Oxygen and the fraction of Helium required in the final mix.
 *
 *  @param fO2 a double argument representing the fraction of Oxygen required.
 *  @param andFractionOfHelium a double argument representing the fraction of Helium in the final mix.
 *  @param error a NSError pointer in the event of initialization failure. The returned
 *              error object will contain a description of the problem,
 *
 *  @return id if successful, otherwise nil
 */
- (id)initAsTriMixWithFO2: (double) fO2 andFractionOfHelium:(double) fHe error:(NSError **)error {
    self = [super init];
    if (self) {
        /**
         * There is not too much we can do other than check for negative values
         * and make sure that the fO2 and fHe do not exceed 1.0
         */
        int tmpFO2 = round(fO2 * 100); // Round out the floaty bits
        int tmpFHe = round(fHe * 100);
        if (tmpFO2 < 0) {
            *error = [NSError errorWithDomain:kFO2TooLow code:FO2_TOO_LOW userInfo:nil];
            return nil;
        } 
        else if(tmpFHe < 0) {
            *error = [NSError errorWithDomain:kFHeTooLow code:FHE_TOO_LOW userInfo:nil];
            return nil;            
        }
        else if (tmpFO2 > 100) {
            *error = [NSError errorWithDomain:kFO2TooHi code:FO2_TOO_HI userInfo:nil];
            return nil;
        }
        else if (tmpFHe > 100) {
            *error = [NSError errorWithDomain:kFHeTooHi code:FH2_TOO_HI userInfo:nil];
            return nil;
        }
        else if ((tmpFO2 + fHe) > 100){
            *error = [NSError errorWithDomain:kImpossibleMix code:IMPOSSIBLE_MIX userInfo:nil];
            return nil;
        }
        
        /***
         * Everything checks out so go ahead and build the object
         */
        oxygenContent = [NSNumber numberWithDouble:fO2];
        heliumContent = [NSNumber numberWithDouble:fHe];
        nitrogenContent = [NSNumber numberWithDouble:1.0f - ( fO2 + fHe)];
    }
    return self;
}

/**
 *  A Standard Gas Constructor. This constructor will generate a Trimix Object
 *      with a standard gas based on the depth provided as an argument. This
 *      method will only choose 30/30 in the range between 9ft-100ft(27m-30m).
 *      Shallower than that it will return a nirtrox object. It the depth 
 *      supplied is greater than the GUE standard gas operating range of
 *      0ft-330ft, 0-100m then the an a null pointer will be returned.
 *
 *  @param depth a double value representing the maximum depth of the dive.
 *  @param isMetric a Boolean value, if TRUE the constructor assumes the depth
 *      provided is metric.
 *  @param error a NSError pointer in the event of initialization failure. The returned
 *      error object will contain a description of the problem,
 *
 *  @return id if successful otherwise nil
 */
- (id) initWithStdGasForDepth:(double) depth isMetric: (BOOL) metric error:(NSError **)error {

    /***
     * User requested a depth less than 3'/1m or greater than 330'/100m
     * This checks to make sure a negative value is not entered as well.
     */
    if (depth < MS_LOWER_DEPTH_LIMIT(metric)) {
        *error = [NSError errorWithDomain:kDepthTooShallow code:DEPTH_TOO_SHALLOW userInfo:nil];
        return nil;
    } else if (depth > MS_UPPER_DEPTH_LIMIT(metric)){
        *error = [NSError errorWithDomain:kDepthTooDeep code:DEPTH_TOO_DEEP userInfo:nil];
        return nil;
    }
     
    int selectedDepth = (int)depth; // Round off the floaty bit
    /***
     *  We can ignore the error codes from the created objects as they are being created
     *  with the "correct" values so there should be no errors.
     */
    self = [super init];
    if (self) {
        if (selectedDepth <= MS_NITROX_RANGE(metric)) {
            [self initAsNitrox:0.32 error:error];
        }
        else if (selectedDepth > MS_NITROX_RANGE(metric) && selectedDepth <= MS_TRIOX_RANGE(metric)) {
            [self initAsTriMixWithFO2:0.30 andFractionOfHelium:0.30 error:error];
        }
        else if (selectedDepth > MS_TRIOX_RANGE(metric) && selectedDepth <= MS_TRIMIX21_35_RANGE(metric)) {
            [self initAsTriMixWithFO2:0.21 andFractionOfHelium:0.35 error:error];
        } 
        else if (selectedDepth > MS_TRIMIX21_35_RANGE(metric) && selectedDepth <= MS_TRIMIX18_45_RANGE(metric)) {
            [self initAsTriMixWithFO2:0.18 andFractionOfHelium:0.45 error:error];
        }
        else if (selectedDepth > MS_TRIMIX18_45_RANGE(metric) && selectedDepth <= MS_TRIMIX15_55_RANGE(metric)) {
            [self initAsTriMixWithFO2:0.15 andFractionOfHelium:0.55 error:error];
        }
        else if (selectedDepth > MS_TRIMIX15_55_RANGE(metric) && selectedDepth <= MS_TRIMIX12_65_RANGE(metric)) {
            [self initAsTriMixWithFO2:0.12 andFractionOfHelium:0.65 error:error];
        }
        else if (selectedDepth > MS_TRIMIX12_65_RANGE(metric) && selectedDepth <= MS_TRIMIX10_70_RANGE(metric)) {
            [self initAsTriMixWithFO2:0.10 andFractionOfHelium:0.70 error:error];
        }
    }
    /***
     * Initialization success
     */
    return self;
}   

/***
 * The following six initializers create standard gas mixes as indicated. They
 * are just wrappers to create canned MixGas objects with the 
 * initAsTriMix construtor.
 */

/**
 *  A Standard Gas Constructor. This constructor will generate a TriOx Object
 *
 *  @return id
 */
- (id) initAsTriOx {
    /***
     * While we have to provide an NSError pointer, we can safely ignore any
     * return values as these objects are being created with the 'correct' values
     */
    NSError *error;
    self = [super init];
    if (self) {
        [self initAsTriMixWithFO2:0.30 andFractionOfHelium:0.30 error:&error];
    }   
    return self;
}

/**
 *  A Standard Gas Constructor. This constructor will generate a 21/35 Object
 *
 *  @return id
 */
- (id) initAs21_35 {
    /***
     * While we have to provide an NSError pointer, we can safely ignore any
     * return values as these objects are being created with the 'correct' values
     */
    NSError *error;
    self = [super init];
    if (self) {
        [self initAsTriMixWithFO2:0.21 andFractionOfHelium:0.35 error:&error];
    }
    return self;    
}

/**
 *  A Standard Gas Constructor. This constructor will generate a 18/45 Object
 *
 *  @return id
 */
- (id) initAs18_45 {
    /***
     * While we have to provide an NSError pointer, we can safely ignore any
     * return values as these objects are being created with the 'correct' values
     */
    NSError *error;
    self = [super init];
    if (self) {
        [self initAsTriMixWithFO2:0.18 andFractionOfHelium:0.45 error:&error];
    }
    return self;    
}

/**
 *  A Standard Gas Constructor. This constructor will generate a 15/55 Object
 *
 *  @return id
 */
- (id) initAs15_55 {
    /***
     * While we have to provide an NSError pointer, we can safely ignore any
     * return values as these objects are being created with the 'correct' values
     */
    NSError *error;
    self = [super init];
    if (self) {
        [self initAsTriMixWithFO2:0.15 andFractionOfHelium:0.55 error:&error];
    }
    return self;    
}

/**
 *  A Standard Gas Constructor. This constructor will generate a 12/65 Object
 *
 *  @return id
 */
- (id) initAs12_65 {
    /***
     * While we have to provide an NSError pointer, we can safely ignore any
     * return values as these objects are being created with the 'correct' values
     */
    NSError *error;
    self = [super init];
    if (self) {
        [self initAsTriMixWithFO2:0.12 andFractionOfHelium:0.65 error:&error];
    }
    return self;    
}

/**
 *  A Standard Gas Constructor. This constructor will generate a 10/70 Object
 *
 *  @return id
 */
- (id) initAs10_70 {
    /***
     * While we have to provide an NSError pointer, we can safely ignore any
     * return values as these objects are being created with the 'correct' values
     */
    NSError *error;
    self = [super init];
    if (self) {
        [self initAsTriMixWithFO2:0.10 andFractionOfHelium:0.70 error:&error];
    }
    return self;    
}


/*********************************************************************************
 * Class Methods
 *********************************************************************************/

/**
 *  Class Method. This method will return the Equivalent Narcotic Depth (END) of a mix
 *
 *  @param fractionOfHelium a double value which represents the helium content
 *  @param depth a double value which contains the depth of the dive
 *  @param isMetric a Boolean value, if TRUE the method assumes the depth
 *      provided is metric.
 *  @return a long value containing the END of the mix
 */
+(long) ENDofGasWithFHe: (double) fractionOfHelium atDepthOf: (double) depth isMetric: (BOOL) metric {
    
    double ata = (depth / METRIC_TEST(metric)) + 1;
    double fractionOfNarcoticGases = 1 - fractionOfHelium;
    double ppOfNarcoticGases = ata * fractionOfNarcoticGases;
    
    return (long)((ppOfNarcoticGases - 1) * (metric ? 10 : 33));
}

/**
 *  Class Method. This method will return the Equivalent Air Depth (EAD) of a mix. This method
 *      should only be used with Nitrox mixes
 *
 *  @param fractionOfNitrogen a double value which represents the nitrogen content
 *  @param depth a double value which contains the depth of the dive
 *  @param isMetric a Boolean value, if TRUE the method assumes the depth
 *      provided is metric.
 *
 *  @return a long value containing the EAD of the mix
 */
+(long) EADofGasWithFN2: (double) fractionOfNitrogen atDepthOf: (double) depth isMetric: (BOOL) metric {    
    
    return (long)(((fractionOfNitrogen * (depth + METRIC_TEST(metric))) / 0.79) - METRIC_TEST(metric));
}

/*********************************************************************************
 * Instance Methods
 *********************************************************************************/

/**
 *  Instance Method. This method will return the Equivalent Narcotic Depth (END) of a Trimix Object. This method
 *      should only makes sense to be used with a Trimix object
 *
 *  @param depth a double value which contains the depth of the dive
 *  @param isMetric a Boolean value, if TRUE the method assumes the depth
 *      provided is metric.
 *
 *  @return a long value containing the END of the Trimix object
 */
-(long) ENDofGas: (double) depth isMetric: (BOOL) metric {
    
    double ata = (depth / METRIC_TEST(metric)) + 1;
    double fractionOfNarcoticGases = 1 - [heliumContent doubleValue];
    double ppOfNarcoticGases = ata * fractionOfNarcoticGases;
    
    return (long)((ppOfNarcoticGases - 1) * (metric ? 10 : 33));
}

/**
 *  Instance Method. This method will return the Equivalent Air Depth (EAD) of a Nitrox Object. This method
 *      should only be used with Nitrox
 *
 *  @param depth a double value which contains the depth of the dive
 *  @param isMetric a Boolean value, if TRUE the method assumes the depth
 *      provided is metric.
 *
 *  @return a long value containing the END of the Nitrox object
 */

-(long) EADofGas: (double) depth isMetric: (BOOL) metric {    
    
    return (long)((([nitrogenContent doubleValue] * (depth + METRIC_TEST(metric))) / 0.79) - METRIC_TEST(metric));
}

/**
 *  Instance Method. This method will calculate the Maximum Operating Depth (MOD) of a GasMix Object.\
 *
 *  @param atPPO2 a double value which represents the maximum ppO2 that you wish to be exposed to. Suggested
 *      values are 1.2 (during the working portion of the dive) and 1.6 (during the deco)
 *  @param isMetric a Boolean value, if TRUE the method returns a value in meters, else the return value
 *      is in feet
 *
 *  @return a double value representing the MOD of the GasMix object
 */
-(double) MOD: (double) atPPO2 isMetric: (BOOL) metric {
    return ((atPPO2 / [oxygenContent doubleValue]) - 1) * (METRIC_TEST(metric));
}

/**
 *  An Instance Method. This method calculates the partial pressure of Oxygen
 *      for a given depth.
 *
 *  @param atDepth a double value of the depth that is being tested
 *  @param isMetric a Boolean value, if TRUE the method assumes the input value
 *      depth is in meters, else depth is in feet
 */
-(double) ppO2: (double) atDepth isMetric: (BOOL) metric {
    return ([oxygenContent doubleValue] * ((atDepth / METRIC_TEST(metric)) + 1));
}

/**
 *  An Instance Method. This method over-rides the default descriptor
 *
 *  @return a NSString that based on whether the object has been
 *      initialized as a Nitrox or Trimix Object. It describers the contents of the mix
 */

-(NSString *) description {
    
    NSString *objDescription;
    
    if (heliumContent > 0) {    // Due to the imprecise nature of doubles compare to 1 rather than 0
        objDescription = [NSString stringWithFormat:@"The the contents of this mix is %1.2f Oxygen, %1.2f Helium, and %1.2f Nitrogen",
                          [oxygenContent doubleValue],
                          [heliumContent doubleValue],
                          [nitrogenContent doubleValue]];
        
    } else {                    // We can assume that it is a Nitrox object
        objDescription = [NSString stringWithFormat:@"The the contents of this mix is %1.2f Oxygen and %1.2f Nitrogen",
                          [oxygenContent doubleValue],
                          [nitrogenContent doubleValue]];
    }
    return objDescription;
}
    

- (void)dealloc
{
    [super dealloc];
}
@end 